/*
 * GDefaultCupImpl.java
 * Created on 21 juin 08
 *
 * Glacéo Internet Platform
 * http://sourceforge.net/projects/chm/
 *
 * Copyright (c) 2005-2008, Jens and Jan Janke (VirtualHockey Project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hockeo.vhbase.data.impl.contest;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Required;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.hockeo.vhbase.data.dao.IGDao;
import com.hockeo.vhbase.data.dao.club.GClub;
import com.hockeo.vhbase.data.dao.club.GClubCupHistoryRecord;
import com.hockeo.vhbase.data.dao.contest.GContest;
import com.hockeo.vhbase.data.dao.contest.GContestElement;
import com.hockeo.vhbase.data.dao.contest.GContestHistoryRecord;
import com.hockeo.vhbase.data.dao.contest.GContestRound;
import com.hockeo.vhbase.data.dao.contest.GKO3ContestElement;
import com.hockeo.vhbase.data.dao.contest.GNationalCup;
import com.hockeo.vhbase.data.dao.match.GKOMatch;
import com.hockeo.vhbase.data.dao.season.GMatchday;
import com.hockeo.vhbase.data.dao.season.GMoveday;
import com.hockeo.vhbase.data.dao.season.GSeason;
import com.hockeo.vhbase.data.dao.standings.GFreeTicket;
import com.hockeo.vhbase.data.dao.standings.GGoalsAccount;
import com.hockeo.vhbase.data.helper.GContestRoundFormat;
import com.hockeo.vhbase.data.helper.GContestRoundType;
import com.hockeo.vhbase.data.impl.IGContestImpl;
import com.hockeo.vhbase.data.impl.goals.IGGoalsDistributor;
import com.hockeo.vhbase.error.GDataException;
import com.hockeo.vhbase.error.GException;


/**
 * Default implementation for national cup competitions.
 *
 * @version $Id$
 * @author jjanke
 */
@Transactional( propagation = Propagation.MANDATORY, rollbackFor = { GException.class } )
public class GDefaultCupImpl implements IGContestImpl
{
  private IGDao              d_dao;
  private IGGoalsDistributor d_goalsDistributor;

  @Required
  public final void setDao( IGDao dao )
  {
    d_dao = dao;
  }

  @Required
  public void setGoalsDistributor( IGGoalsDistributor goalsDistributor )
  {
    d_goalsDistributor = goalsDistributor;
  }

  /**
   * Creates the contest structure for a normal national cup contest. It basically
   * identifies how many rounds are required to play the whole national cup competition
   * considering that all teams of the first league (cat 1) enter the competition in the
   * round of the last N teams (see contest#numTeams for the exact number). All other
   * teams play as many rounds as necessary to complete the round of the last N.
   *
   * @see com.hockeo.vhbase.data.impl.IGContestImpl#createStructure(com.hockeo.vhbase.data.dao.contest.GContest)
   */
  public void createStructure( final GSeason season, final GContest contest ) throws GDataException
  {
    GNationalCup cup = d_dao.findEntity( GNationalCup.class, contest.getId() );
    int nNumClubs = (int) d_dao.findNumClubs( contest.getAssociation() );
    int nNumClubsCat1 = (int) d_dao.findNumClubs( contest.getAssociation(), 1 );
    int nNumClubsNonCat1 = nNumClubs - nNumClubsCat1;

    // Let's start with the qualification
    //
    int nNumPrevRounds = createQualificationTournament( season, cup, nNumClubs, nNumClubsCat1, nNumClubsNonCat1 );

    // If no rounds have been created, all clubs will be attached to the first main round
    //
    boolean fAttachAllClubsToFirstMainRound = nNumPrevRounds == 0;

    // Create the real competition
    //
    int nNumParticipants = nNumClubs < cup.getNumTeams() ? nNumClubs : cup.getNumTeams();
    ArrayList<GContestRound> listMainRounds = createRounds( contest, nNumPrevRounds, 1, nNumParticipants );

    assert !listMainRounds.isEmpty() : "The list of created main rounds should never be empty!";

    // Assign correct round types to the created rounds and assign loser ranks to each
    // contest element
    //
    int nRemainingClubs = nNumClubs;
    int nLastRound = cup.getContestRounds().size();
    for ( GContestRound round : cup.getContestRounds() )
    {
      d_dao.makePersistent( round );

      GKO3ContestElement element = (GKO3ContestElement) round.getContestElements().get( 0 );
      nRemainingClubs -= element.getNumMatches();

      if ( nRemainingClubs == 1 )
        round.setBestPossibleLoserRank( 2 );
      else
        round.setBestPossibleLoserRank( nRemainingClubs + 1 );

      round.setRoundType( GContestRoundType.getTypeForNationalCup( round.getRoundNum(), nLastRound ) );
      round.setRemainingKOMatchesSameGA( ( nLastRound - round.getRoundNum() ) * 2 );
      round.setLastRound( nLastRound == round.getRoundNum() );
    }

    // Attach either all or only the level-1 clubs to the first main round
    //
    if ( fAttachAllClubsToFirstMainRound )
      assignClubsToCupRound( season, listMainRounds.get( 0 ), 1, 99 );
    else
      assignClubsToCupRound( season, listMainRounds.get( 0 ), 1, 1 );
  }

  public void undoCreateStructure( GSeason season, GContest contest ) throws GDataException
  {
    // Remove club/cup history elements
    //
    d_dao.deleteClubHistoryRecords( season, contest );

    // Remove contest rounds (elements will be removed automatically because of the
    // cascaded removal)
    //
    if ( contest.getContestRounds() != null )
      for ( GContestRound round : contest.getContestRounds() )
        d_dao.delete( round );
  }

  /**
   * Deletes all created cup contest rounds with the dependent contest elements.
   */
  public void deleteStructure( final GSeason season, final GContest contest ) throws GDataException
  {
    // Remove contest rounds (elements will be removed automatically because of the
    // cascaded removal)
    //
    if ( contest.getContestRounds() != null )
      for ( GContestRound round : contest.getContestRounds() )
        d_dao.delete( round );
  }

  /*
   * @see com.hockeo.vhbase.data.impl.IGContestImpl#createMatchdays(com.hockeo.vhbase.data.GContest)
   */
  public void createMatchdays( GContest contest ) throws GDataException
  {
    // We have to make sure that the matchdays are correctly numbered from 1 to N
    //
    int nMatchdayCnt = 0;

    for ( GContestRound round : contest.getContestRounds() )
    {
      int nMaxMatchdaysPerRound = 0;

      for ( GContestElement element : round.getContestElements() )
      {
        int i = 1;
        int nNumMatchdays = element.getNumMatchdays();
        for ( ; i <= nNumMatchdays; i++ )
        {
          GMatchday matchday = new GMatchday( element, nMatchdayCnt + i, i == 1, i == nNumMatchdays );

          d_dao.makePersistent( matchday );
        }

        if ( i - 1 > nMaxMatchdaysPerRound )
          nMaxMatchdaysPerRound = i - 1;
      }

      nMatchdayCnt += nMaxMatchdaysPerRound;
    }
  }

  /*
   * @see com.hockeo.vhbase.data.impl.IGContestImpl#assignMatchdaysToMovedays(com.hockeo.vhbase.data.GContest)
   */
  public void assignMatchdaysToMovedays( GContest contest ) throws GDataException
  {
    GNationalCup cup = d_dao.findEntity( GNationalCup.class, contest.getId() );
    int nNumRoundsTotal = cup.getContestRounds().size();
    int nNumRoundsMain = cup.getNumRoundsMainContest();
    int nNumRoundsQuali = nNumRoundsTotal - nNumRoundsMain;

    // Get the reserved movedays and make sure that we have exactly the right number of
    // them (if necessary, unnecessary movedays will be removed from the list)
    //
    List<GMoveday> listAvailableMovedays = d_dao.findReservedMovedays( cup );

    if ( listAvailableMovedays.size() < nNumRoundsTotal )
      throw new GDataException( "There are not enough reserved movedays for national cups available.\nThe cup "
          + cup.getId() + " requires " + nNumRoundsTotal + " movedays, but only " + listAvailableMovedays.size()
          + " are available." );
    else if ( listAvailableMovedays.size() > nNumRoundsTotal )
    {
      int nToBeRemoved = listAvailableMovedays.size() - nNumRoundsTotal;

      // We can remove movedays because we have more than needed. If we have qualification
      // rounds, we remove them at the end of the qualification. Otherwise at the
      // beginning of the whole contest.
      //
      if ( nNumRoundsQuali > 0 )
        for ( int i = 0; i < nToBeRemoved; i++ )
          listAvailableMovedays.remove( nNumRoundsQuali );
      else
        for ( int i = 0; i < nToBeRemoved; i++ )
          listAvailableMovedays.remove( 0 );
    }

    // Do the actual attachment of matchdays
    //
    for ( GContestRound round : cup )
      for ( GContestElement element : round )
        for ( GMatchday matchday : d_dao.findMatchdays( element ) )
          listAvailableMovedays.get( round.getRoundNum() - 1 ).addMatchday( matchday );
  }

  public void createNextContestRound( final GContest contest, final GSeason season, final GMoveday movedayCurrent )
      throws GDataException
  {
    GContestRound round = d_dao.findFirstNonPlayedContestRound( contest );

    // If all rounds are played, nothing needs to be created anymore
    //
    if ( round == null )
      return;

    // KO rounds must not have more than one contest element
    //
    GKO3ContestElement element = (GKO3ContestElement) round.getContestElements().get( 0 );

    d_goalsDistributor.initialize( element );
    GContestImplUtils.createGoalsAccounts( d_dao, d_goalsDistributor, season, element, true );
    GContestImplUtils.createMatches( d_dao, season, element, true );
  }

  /*
   * @see com.hockeo.vhbase.data.impl.IGContestImpl#undoCreateNextContestRound(com.hockeo.vhbase.data.GContest,
   * com.hockeo.vhbase.data.GMoveday)
   */
  public void undoCreateNextContestRound( final GContest contest, final GMoveday movedayLastPrepared )
      throws GDataException
  {
    GContestRound round = d_dao.findFirstNonPlayedContestRound( contest );

    if ( round == null )
      return;

    // KO rounds must not have more than one contest element
    //
    d_dao.deleteGoalAccountsMatchesFreeTicketsAndStandings( round.getContestElements().get( 0 ) );
  }

  /**
   * Determines the winners of all KO matches attached to the given contest element if
   * this method is called for the last matchday of the current round. The winners are
   * then attached to the next contest round (if there is one).
   *
   * @see com.hockeo.vhbase.data.impl.IGContestImpl#analyseMatches(com.hockeo.vhbase.data.dao.contest.GContestElement,
   *      com.hockeo.vhbase.data.dao.season.GMatchday)
   */
  public void analyseMatches( GContestElement contestElement, GMatchday matchday ) throws GDataException
  {
    if ( matchday.isFirst() )
      contestElement.getContestRound().setStarted( true );

    // The actual analysis is only done when the last matchday of the contest element is
    // passed
    //
    if ( !matchday.isLast() )
    {
      matchday.setAnalysed( true );
      return;
    }

    final GSeason season = d_dao.findCurrentSeason();
    final GKO3ContestElement element = d_dao.findEntity( GKO3ContestElement.class, contestElement.getId() );
    final GContestRound roundNext = element.getContest().getNextRound( element.getContestRound() );
    final Map<GClub, GGoalsAccount> mapGAs = d_dao.findGoalsAccountsAsMap( contestElement );
    final Map<GClub, GClubCupHistoryRecord> mapCHRs = d_dao.findClubHistoryRecordsByContestElement(
        GClubCupHistoryRecord.class, season, element );
    final ArrayList<GClubCupHistoryRecord> listLosers = new ArrayList<GClubCupHistoryRecord>();

    for ( GKOMatch komatch : element.getKOMatches() )
    {
      komatch.play( mapGAs.get( komatch.getClubHome() ), mapGAs.get( komatch.getClubAway() ) );

      // Create/update club/cup history records for home team and possibly assign it to
      // next contest round
      //
      GClubCupHistoryRecord chr = mapCHRs.get( komatch.getClubHome() );
      chr.addRoundResult( element, komatch );
      if ( roundNext != null && komatch.isWinner( komatch.getClubHome() ) )
        chr.assignToRound( roundNext );

      if ( komatch.isLoser( komatch.getClubHome() ) )
        listLosers.add( chr );

      // Create/update club/cup history records for away team and possibly assign it to
      // next contest round
      //
      chr = mapCHRs.get( komatch.getClubAway() );
      chr.addRoundResult( element, komatch );
      if ( roundNext != null && komatch.isWinner( komatch.getClubAway() ) )
        chr.assignToRound( roundNext );

      if ( komatch.isLoser( komatch.getClubAway() ) )
        listLosers.add( chr );
    }

    // Assign free ticket holders to next round (if there is one)
    //
    if ( roundNext != null )
      for ( GFreeTicket ft : element.getContestRound().getFreeTickets() )
        d_dao.findClubHistoryRecord( GClubCupHistoryRecord.class, element.getContest(), season, ft.getClub() ).assignToRound(
            roundNext );

    // Establish more precise ranking of all losers
    //
    if ( !contestElement.getContestRound().isLastRound() )
    {
      Collections.sort( listLosers );
      int nRank = 0;
      int nNum = contestElement.getContestRound().getBestPossibleLoserRank();
      GClubCupHistoryRecord chrPrevLoser = null;
      for ( GClubCupHistoryRecord chrLoser : listLosers )
      {
        if ( chrPrevLoser == null || chrLoser.compareTo( chrPrevLoser ) != 0 )
          nRank = nNum;

        chrLoser.setRank( nRank );
        chrPrevLoser = chrLoser;
        nNum++;
      }
    }
  }

  public void postprocessMultiElementRound( GContestRound round, Integer nMatchday ) throws GDataException
  {
    // Currently, there are no national cups with more than 1 contest element.
  }

  /*
   * @see com.hockeo.vhbase.data.impl.IGContestImpl#resetGoalAccounts(com.hockeo.vhbase.data.GMoveday,
   * com.hockeo.vhbase.data.GContestElement)
   */
  public void resetGoalAccounts( final GMoveday moveday, final GContestElement element ) throws GDataException
  {
    GContestImplUtils.resetGoalAccounts( d_dao, moveday, d_dao.findEntity( GKO3ContestElement.class, element.getId() ) );
  }

  /**
   * Updates club/cup history records so that the results of the last moveday are not
   * considered anymore.
   *
   * @see com.hockeo.vhbase.data.impl.IGContestImpl#resetContestStats(com.hockeo.vhbase.data.dao.season.GMoveday,
   *      com.hockeo.vhbase.data.dao.contest.GContestElement)
   */
  public void resetContestStats( GMoveday moveday, GContestElement element ) throws GDataException
  {
    final GSeason season = d_dao.findCurrentSeason();
    final GKO3ContestElement koelement = d_dao.findEntity( GKO3ContestElement.class, element.getId() );
    final Map<GClub, GClubCupHistoryRecord> mapCHRs = d_dao.findClubHistoryRecordsByContestElement(
        GClubCupHistoryRecord.class, season, element );

    // Remove stats from current round from the club/cup history records
    //
    for ( GKOMatch komatch : koelement.getKOMatches() )
    {
      // Home club (reset history and assign it to current round)
      //
      GClubCupHistoryRecord chr = mapCHRs.get( komatch.getClubHome() );

      if ( chr.getFirstRound() == koelement.getContestRound().getRoundNum() )
        chr.reset();
      else
        chr.removeRoundResult( koelement, komatch );

      chr.assignToRound( element.getContestRound() );

      // Away club (reset history and assign it to current round)
      //
      chr = mapCHRs.get( komatch.getClubAway() );

      if ( chr.getFirstRound() == koelement.getContestRound().getRoundNum() )
        chr.reset();
      else
        chr.removeRoundResult( koelement, komatch );

      chr.assignToRound( element.getContestRound() );
    }

    // Reassign free ticket holders to current contest round
    //
    for ( GFreeTicket ft : element.getContestRound().getFreeTickets() )
      d_dao.findClubHistoryRecord( GClubCupHistoryRecord.class, element.getContest(), season, ft.getClub() ).assignToRound(
          element.getContestRound() );
  }

  /**
   * Creates contest history record. The club/cup history elements do not need to be
   * created by this method because they are created and immediately updated after each
   * cup round.
   *
   * @see com.hockeo.vhbase.data.impl.IGContestImpl#createEndOfSeasonStatistics(com.hockeo.vhbase.data.dao.contest.GContestElement
   *      )
   */
  public void createEndOfSeasonStatistics( GContestElement contestElement ) throws GDataException
  {
    if ( !contestElement.getContestRound().isLastRound() )
      return;

    GSeason season = d_dao.findCurrentSeason();
    GNationalCup cup = d_dao.findEntity( GNationalCup.class, contestElement.getContest().getId() );
    GKO3ContestElement element = d_dao.findEntity( GKO3ContestElement.class, contestElement.getId() );

    // Make sure that no statistics have been created so far
    //
    if ( d_dao.findContestHistoryRecord( season, cup ) != null )
      return;

    // Contest history record
    //
    GKOMatch komatchFinal = element.getKOMatches().get( 0 );
    GContestHistoryRecord chr = new GContestHistoryRecord( cup.getHistoryType(), season, cup,
        komatchFinal.getWinner(), komatchFinal.getLoser() );

    d_dao.makePersistent( chr );
  }

  /**
   * Deletes the possibly existing contest history record.
   *
   * @see com.hockeo.vhbase.data.impl.IGContestImpl#deleteEndOfSeasonStatistics(com.hockeo.vhbase.data.dao.contest.GContestElement,
   *      boolean)
   */
  public void deleteEndOfSeasonStatistics( final GContestElement contestElement ) throws GDataException
  {
    if ( !contestElement.getContestRound().isLastRound() )
      return;

    GSeason season = d_dao.findCurrentSeason();

    GContestHistoryRecord chr = d_dao.findContestHistoryRecord( season, contestElement.getContest() );
    if ( chr != null )
      d_dao.delete( chr );
  }

  /**
   * Creates the qualification tournament for the national cup competition.
   *
   * @param season the season for which to create the qualification tournament
   * @param cup the cup for which to create the qualification tournament
   * @param nNumAllClubs the number of all clubs qualified for the national cup
   *          competition (incl. those that are directly qualified for the first main
   *          round)
   * @param nNumClubsCat1 the number of clubs directly qualified for the first round
   * @param nNumClubsNonCat1 the number of clubs not directly qualified for the first main
   *          round
   * @return the number of created qualification rounds
   * @throws GDataException
   */
  private int createQualificationTournament( GSeason season, GNationalCup cup, int nNumAllClubs, int nNumClubsCat1,
      int nNumClubsNonCat1 ) throws GDataException
  {
    if ( nNumAllClubs <= cup.getNumTeams() )
      return 0;

    if ( nNumClubsCat1 >= cup.getNumTeams() )
      throw new GDataException( "There are more or equal than " + cup.getNumTeams()
          + " first league clubs in the association " + cup.getAssociation().getId()
          + ". The national cup contest using the implementation " + getClass().getName()
          + " only supports creating cup competitions with a maximum of " + ( cup.getNumTeams() - 1 )
          + " first league teams for the mentioned association." );

    // Number of winning teams at the end of the contest
    //
    int nNumQualifiers = cup.getNumTeams() - nNumClubsCat1;

    // Number of teams that are qualified for the next round
    //
    int nNumParticipants = nNumClubsNonCat1;

    // Create the different rounds until we have the number of necessary qualifiers
    //
    ArrayList<GContestRound> listRounds = createRounds( cup, 0, nNumQualifiers, nNumParticipants );

    assert !listRounds.isEmpty() : "The list of created rounds should never be empty!";

    // Attach clubs being part of a non-first level league to first qualifying round (99
    // to make sure all league levels are considered)
    //
    assignClubsToCupRound( season, listRounds.get( 0 ), 2, 99 );

    // Return the number of created rounds
    //
    return listRounds.size();
  }

  /**
   * Creates the KO rounds that are necessary to determine the given number of winners.
   *
   * @param contest the contest to which the created rounds should be attached
   * @param nNumPreviousRounds the number of previous rounds in the competition (used to
   *          compute the round numbers of the created rounds)
   * @param nNumWinners the number of winning teams
   * @param nNumParticipants the number of initial participants
   * @return a list of created contest rounds. The created rounds are not persisted to the
   *         database.
   */
  private ArrayList<GContestRound> createRounds( GContest contest, int nNumPreviousRounds, int nNumWinners,
      int nNumParticipants )
  {
    int nRound = 1;
    boolean fLastRoundCreated = false;
    ArrayList<Integer> listMatchesPerRound = new ArrayList<Integer>();
    ArrayList<GContestRound> listCreatedRounds = new ArrayList<GContestRound>();

    while ( !fLastRoundCreated )
    {
      int nNumRoundMatches = (int) Math.pow( 2, nRound - 1 ) * nNumWinners;
      int nNumRoundClubs = nNumRoundMatches * 2;

      // In the last round (in reality played first) there may not be enough teams left to
      // play a full round. Some teams will be awarded a free ticket (a laisser-passer)
      // and there are less matches than theoretically foreseen
      //
      if ( nNumRoundClubs > nNumParticipants )
      {
        nNumRoundMatches = nNumParticipants - nNumRoundMatches;
        nNumRoundClubs = nNumRoundMatches * 2;
        fLastRoundCreated = true;
      }
      // If the number of round clubs is equal to the number of participants we have
      // created the last round
      //
      else if ( nNumRoundClubs == nNumParticipants )
        fLastRoundCreated = true;

      listMatchesPerRound.add( Integer.valueOf( nNumRoundMatches ) );
      nRound++;
    }

    Collections.reverse( listMatchesPerRound );
    for ( int i = 0; i < listMatchesPerRound.size(); i++ )
    {
      int nNumRound = nNumPreviousRounds + i + 1;
      GContestRound round = new GContestRound( contest, contest.getId() + "-" + nNumRound, nNumRound,
          GContestRoundType.NOT_DEFINED, GContestRoundFormat.KNOCK_OUT, false );
      GKO3ContestElement element = new GKO3ContestElement( round, round.getId() + "-1", "", listMatchesPerRound.get( i ) );

      round.addContestElement( element );
      listCreatedRounds.add( round );
    }

    return listCreatedRounds;
  }

  /**
   * Assigns all clubs from the association to the national cup round specified as
   * parameter. Only clubs being attached to leagues in the specified category range are
   * considered. The clubs are assigned by creating club/cup history objects.
   *
   * @param round the national cup contest round to which the clubs should be assigned
   * @param nLeagueCatFrom the start of the league category interval
   * @param nLeagueCatTo the end of the league category interval
   */
  private void assignClubsToCupRound( GSeason season, GContestRound round, int nLeagueCatFrom, int nLeagueCatTo )
  {
    List<GClub> listClubs = d_dao.findClubsForLeagueCategories( round.getContest().getAssociation(), nLeagueCatFrom,
        nLeagueCatTo );

    for ( GClub club : listClubs )
      d_dao.makePersistent( new GClubCupHistoryRecord( season, club, round ) );
  }
}
